CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/vouchers/[id].tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { useEffect, useMemo, useState } from "react";
7
import Footer from "components/landing/footer";
8
import Header from "components/landing/header";
9
import Head from "components/landing/head";
10
import {
11
Alert,
12
Button,
13
Card,
14
Divider,
15
Layout,
16
Modal,
17
Space,
18
Table,
19
} from "antd";
20
import withCustomize from "lib/with-customize";
21
import { Customize } from "lib/customize";
22
import { Icon } from "@cocalc/frontend/components/icon";
23
import A from "components/misc/A";
24
import Loading from "components/share/loading";
25
import TimeAgo from "timeago-react";
26
import apiPost from "lib/api/post";
27
import Avatar from "components/account/avatar";
28
import type { VoucherCode } from "@cocalc/util/db-schema/vouchers";
29
import { stringify as csvStringify } from "csv-stringify/sync";
30
import { human_readable_size } from "@cocalc/util/misc";
31
import CodeMirror from "components/share/codemirror";
32
import { trunc } from "lib/share/util";
33
import useDatabase from "lib/hooks/database";
34
import Notes from "./notes";
35
import Help from "components/vouchers/help";
36
import Copyable from "components/misc/copyable";
37
import { DescriptionColumn } from "components/store/cart";
38
39
function RedeemURL({ code }) {
40
const [url, setUrl] = useState<string>("");
41
useEffect(() => {
42
if (typeof window !== "undefined") {
43
setUrl(codeToUrl(code, window.location.href));
44
}
45
}, []);
46
47
return (
48
<Space>
49
<A href={url}>
50
<Icon name="external-link" />
51
</A>{" "}
52
<Copyable display={`…${code}`} value={url} />
53
</Space>
54
);
55
}
56
57
const COLUMNS = [
58
{
59
title: "Redeem URL (share this)",
60
dataIndex: "url",
61
key: "redeem",
62
render: (_, { code }) => <RedeemURL code={code} />,
63
},
64
{
65
title: "Code",
66
dataIndex: "code",
67
key: "code",
68
},
69
{
70
title: "Created",
71
dataIndex: "created",
72
key: "created",
73
align: "center",
74
render: (_, { created }) => (
75
<>{created == null ? "-" : <TimeAgo datetime={created} />}</>
76
),
77
},
78
{
79
title: "When Redeemed",
80
dataIndex: "when_redeemed",
81
key: "when_redeemed",
82
align: "center",
83
render: (_, { when_redeemed }) => (
84
<>{when_redeemed == null ? "-" : <TimeAgo datetime={when_redeemed} />}</>
85
),
86
},
87
{
88
title: "Redeemed By",
89
dataIndex: "redeemed_by",
90
key: "redeemed_by",
91
align: "center",
92
render: (_, { redeemed_by }) => (
93
<>{redeemed_by ? <Avatar account_id={redeemed_by} /> : undefined}</>
94
),
95
},
96
97
{
98
title: "Canceled",
99
dataIndex: "canceled",
100
key: "canceled",
101
align: "center",
102
render: (_, { canceled }) => (canceled ? "Yes" : "-"),
103
},
104
{
105
title: "Your Private Notes",
106
dataIndex: "notes",
107
key: "notes",
108
render: (_, { notes, code }) => <Notes notes={notes} code={code} />,
109
},
110
] as any;
111
112
type DownloadType = "csv" | "json";
113
114
export default function VoucherCodes({ customize, id }) {
115
const database = useDatabase({ vouchers: { id, title: null, cart: null } });
116
const [error, setError] = useState<string>("");
117
const [loading, setLoading] = useState<boolean>(true);
118
const [data, setData] = useState<VoucherCode[] | null>(null);
119
const [showModal, setShowModal] = useState<DownloadType | null>(null);
120
121
useEffect(() => {
122
setLoading(true);
123
(async () => {
124
try {
125
const { codes } = await apiPost("/vouchers/get-voucher-codes", { id });
126
setData(codes);
127
} catch (err) {
128
setError(`${err}`);
129
} finally {
130
setLoading(false);
131
}
132
})();
133
}, []);
134
135
const allCodes = useMemo(() => {
136
if (!data) return [];
137
return data.map((x) => x.code);
138
}, [data]);
139
140
const unusedCodes = useMemo(() => {
141
if (!data) return [];
142
return data.filter((x) => !x.when_redeemed).map((x) => x.code);
143
}, [data]);
144
145
const usedCodes = useMemo(() => {
146
if (!data) return [];
147
return data.filter((x) => !!x.when_redeemed).map((x) => x.code);
148
}, [data]);
149
150
return (
151
<Customize value={customize}>
152
<Head title={`Voucher With id=${id}`} />
153
<DownloadModal
154
data={data}
155
id={id}
156
type={showModal}
157
onClose={() => setShowModal(null)}
158
/>
159
<Layout>
160
<Header />
161
<Layout.Content>
162
<div
163
style={{
164
width: "100%",
165
margin: "10vh 0",
166
display: "flex",
167
justifyContent: "center",
168
}}
169
>
170
<Card style={{ background: "#fafafa" }}>
171
<Space direction="vertical" align="center">
172
<A href="/vouchers">
173
<Icon name="gift2" style={{ fontSize: "75px" }} />
174
</A>
175
<h1>Voucher: id={id}</h1>
176
{database.value?.vouchers?.title && (
177
<h3>Title: {database.value.vouchers.title}</h3>
178
)}
179
<div
180
style={{
181
width: "min(600px, 100vw)",
182
margin: "auto",
183
padding: "15px",
184
}}
185
>
186
{database.value?.vouchers?.cart?.map((item, n) => (
187
<DescriptionColumn key={n} {...item} readOnly />
188
))}
189
</div>
190
<Divider />
191
192
{error && (
193
<Alert
194
type="error"
195
message={error}
196
showIcon
197
style={{ width: "100%", marginBottom: "30px" }}
198
closable
199
onClose={() => setError("")}
200
/>
201
)}
202
{loading && <Loading />}
203
{!loading && data && (
204
<div>
205
<div
206
style={{
207
display: "flex",
208
justifyContent: "center",
209
marginBottom: "15px",
210
}}
211
>
212
<Space direction="vertical">
213
<Space>
214
<div style={{ width: "200px" }}>
215
Copy All Codes {`(${allCodes.length})`}
216
</div>
217
<Copyable
218
value={allCodes.join(", ")}
219
inputWidth={"200px"}
220
/>
221
</Space>
222
<Space>
223
<div style={{ width: "200px" }}>
224
Copy Unused Codes {`(${unusedCodes.length})`}
225
</div>
226
<Copyable
227
value={unusedCodes.join(", ")}
228
inputWidth={"200px"}
229
/>
230
</Space>
231
<Space>
232
<div style={{ width: "200px" }}>
233
Copy Redeemed Codes {`(${usedCodes.length})`}
234
</div>
235
<Copyable
236
value={usedCodes.join(", ")}
237
inputWidth={"200px"}
238
/>
239
</Space>
240
<Space>
241
<div style={{ width: "200px" }}>
242
Export all data to CSV
243
</div>
244
<Button onClick={() => setShowModal("csv")}>
245
<Icon name="csv" /> Export to CSV...
246
</Button>
247
</Space>
248
<Space>
249
<div style={{ width: "200px" }}>
250
Export all data to JSON
251
</div>
252
<Button onClick={() => setShowModal("json")}>
253
<Icon name="js-square" /> Export to JSON...
254
</Button>
255
</Space>
256
</Space>
257
</div>
258
259
<Table
260
columns={COLUMNS}
261
dataSource={data}
262
rowKey="code"
263
pagination={{ defaultPageSize: 50 }}
264
/>
265
</div>
266
)}
267
{!loading && data?.length == 0 && (
268
<div>
269
You have not <A href="/redeem">redeemed any vouchers</A>{" "}
270
yet.
271
</div>
272
)}
273
<Help />
274
</Space>
275
</Card>
276
</div>
277
<Footer />
278
</Layout.Content>
279
</Layout>
280
</Customize>
281
);
282
}
283
284
export async function getServerSideProps(context) {
285
const { id } = context.params;
286
return await withCustomize({ context, props: { id } });
287
}
288
289
function DownloadModal({ type, data, id, onClose }) {
290
const [data0, setData0] = useState<VoucherCode[] | null>(data);
291
useEffect(() => {
292
if (data == null) return;
293
if (typeof window == "undefined") return;
294
setData0(
295
data.map((x) => {
296
return { ...x, url: codeToUrl(x.code, window.location.href) };
297
}),
298
);
299
}, [data]);
300
const path = `vouchers-${id}.${type}`;
301
const content = useMemo(() => {
302
if (!type || data0 == null) return "";
303
if (type == "csv") {
304
const x = [COLUMNS.map((x) => x.title)].concat(
305
data0.map((x) => COLUMNS.map((c) => x[c.dataIndex])),
306
);
307
return csvStringify(x);
308
} else if (type == "json") {
309
return JSON.stringify(data0, undefined, 2);
310
}
311
return "";
312
}, [type, data0]);
313
314
const body = useMemo(() => {
315
if (!type || !data) {
316
return null;
317
}
318
return (
319
<div>
320
<div style={{ margin: "30px", fontSize: "13pt", textAlign: "center" }}>
321
<a
322
href={URL.createObjectURL(
323
new Blob([content], { type: "text/plain" }),
324
)}
325
download={path}
326
>
327
Download {path} (size: {human_readable_size(content.length)})
328
</a>
329
</div>
330
<CodeMirror
331
lineNumbers={false}
332
content={trunc(content, 500)}
333
filename={path}
334
/>
335
</div>
336
);
337
}, [type, data, id]);
338
339
return (
340
<Modal
341
open={type != null}
342
onCancel={onClose}
343
onOk={onClose}
344
title={<>Export all data to {type ? type.toUpperCase() : ""}</>}
345
>
346
{body}
347
</Modal>
348
);
349
}
350
351
function codeToUrl(code, href): string {
352
let i = href.lastIndexOf("/");
353
i = href.lastIndexOf("/", i - 1);
354
return `${href.slice(0, i)}/redeem/${code}`;
355
}
356
357